package ru.ifmo.ctd.intsys.afanasyeva.neural;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Multi-layered neural network, which is trained by back propagation algorithm.
 * 
 * @author Arina Afanasyeva
 */
public class MyNeuralNetwork implements NeuralNetwork {
	private final NonLinearPerceptron[][] perceptrons;
	private double eta;
	private int numberOfSteps;
	
	public void save(String filename) throws FileNotFoundException {
		PrintWriter writer = new PrintWriter(new File(filename));
		for (int i = 0; i < perceptrons.length; i++) {
			for (int j = 0; j < perceptrons[i].length; j++) {
				perceptrons[i][j].print(writer);
				writer.println();
				writer.flush();
			}
		}
		writer.close();
	}
	
	public void load(String filename) throws IOException {
		BufferedReader reader = new BufferedReader(new FileReader(new File(filename)));
		for (int i = 0; i < perceptrons.length; i++) {
			for (int j = 0; j < perceptrons[i].length; j++) {
				perceptrons[i][j].load(reader.readLine());
			}
		}
		reader.close();
	}
	
	/**
	 * Constructs {@link MyNeuralNetwork} with the specified number of input
	 * values and the specified number of perceptrons on each layer
	 * @param perceptronNumber the numbers of perceptrons on each layer
	 * @param inputCount the number of input values
	 * @param eta constant which influences the learning speed	
	 * @param numberOfSteps number of learning cycles
	 */
	public MyNeuralNetwork(int[] perceptronNumber, int inputCount, double eta, int numberOfSteps) {
		
		if(perceptronNumber.length == 0) {
			throw new IllegalArgumentException("Zero number of layers.");
		}
		
		if(inputCount <= 0) {
			throw new IllegalArgumentException("Negative or zero number of inputs.");
		}
		
		for (int i = 0; i < perceptronNumber.length; i++) {
			if (perceptronNumber[i] <= 0) {
				throw new IllegalArgumentException("Wrong number of perceptrons in a layer");
			}
		}
		
		this.eta = eta;
		this.numberOfSteps = numberOfSteps;
		
		perceptrons = new NonLinearPerceptron[perceptronNumber.length][];
		for (int i = 0; i < perceptrons.length; i++) {
			perceptrons[i] = new NonLinearPerceptron[perceptronNumber[i]];
			int num = (i == 0) ? inputCount : perceptrons[i - 1].length;
			for (int j = 0; j < perceptrons[i].length; j++) {
				perceptrons[i][j] = new NonLinearPerceptron(num);
			}
		}
	}
	
	@Override
	public void reset() {
		for (int i = 0; i < perceptrons.length; i++) {
			for (int j = 0; j < perceptrons[i].length; j++) {
				perceptrons[i][j].reset();
			}
		}
	}
	
	/**
	 * Trains this network with the specified examples
	 * @param numberOfSteps number of training cycles
	 * @param inputs example input values
	 * @param answers ideal output values corresponding to the <code>inputs</code>
	 */
	@Override
	public void train(double[][] inputs, double[][] answers) {
		if (inputs.length != answers.length) {
			throw new IllegalArgumentException("The number of inputs doesn't match the number of answers.");
		}
		
		for (int i = 0; i < inputs.length; i++) {
			if (inputs[i].length != perceptrons[0][0].getInputCount()) {
				throw new IllegalArgumentException("Wrong number of inputs.");
			}
		}
		
		for (int i = 0; i < answers.length; i++) {
			if (answers[i].length != perceptrons[perceptrons.length - 1].length) {
				throw new IllegalArgumentException("Wrong number of answers");
			}
		}
		
		for (int i = 0; i < numberOfSteps; i++) {
			for (int j = 0; j < inputs.length; j++) {
				oneStepTrain(inputs[j], answers[j]);
			}
		}
	}	
	
	/**
	 * Calculates output values corresponding to the specified input values
	 * @param inputs the specified input values
	 * @return output values corresponding to the <code>inputs</code>
	 */
	@Override
	public double[] getOutput(double[] inputs) {
		if(inputs.length != perceptrons[0][0].getInputCount()) {
			throw new IllegalArgumentException("wrong number of inputs");
		}
		return calculateOutputs(inputs)[perceptrons.length - 1];
	}
	
	private void oneStepTrain(double[] inputs, double[] answers) {
		double[][] outputs = calculateOutputs(inputs);
		final List<Double> deltas = new ArrayList<Double>();
		final List<Double> previousDeltas = new ArrayList<Double>();
		
		for (int i = perceptrons.length - 1; i >= 0; i--) {
			for (int j = 0; j < perceptrons[i].length; j++) {
				double delta;
				final double output = outputs[i][j];
				if (i == perceptrons.length - 1) {
					delta = answers[j] - output;
				} else {
					delta = 0;
					for (int k = 0; k < perceptrons[i + 1].length; k++) {
						delta += previousDeltas.get(k) * perceptrons[i + 1][k].getWeight(j);
					}
				}
				
				delta *= output * (1 - output);
				deltas.add(delta);
				
				for (int k = 0; k < perceptrons[i][j].getInputCount(); k++) {
					final double in = (i == 0) ? inputs[k] : outputs[i - 1][k];
					perceptrons[i][j].setWeight(k, perceptrons[i][j].getWeight(k) + eta * delta * in);
				}
			}
			
			previousDeltas.clear();
			previousDeltas.addAll(deltas);
			deltas.clear();
		}
	}
	
	private double[][] calculateOutputs(double[] inputs) {
		final double[][] result = new double[perceptrons.length][];
		for (int i = 0; i < perceptrons.length; i++) {
			result[i] = new double[perceptrons[i].length];
			for (int j = 0; j < perceptrons[i].length; j++) {
				final double[] in = (i == 0) ? inputs : result[i - 1];
				result[i][j] = perceptrons[i][j].getOutput(in);
			}
		}
		return result;
	}
}
